import sys
from typing import Union, Dict, Any, List, TypedDict, Tuple, Optional
from enum import Enum

from sqlalchemy import exists
from sqlalchemy.orm import joinedload

sys.path.append('..')
from .main import engine, session
from .models import Topic, Task


class TaskObject(TypedDict):
    """ Описываем поля для объекта задачи с ресурса codeforces """

    link: str
    number: str
    name: str
    topics: List[str]
    complexity: int
    number_of_solutions: int


class TaskFilteringAlgorithm(Enum):
    """ Варианты алгоритмов для фильтрации задач """

    basic_filtering_algorithm = 'basic_filtering_algorithm'


class DbUtils:
    """ Инструменты для работы с базой данных """

    def check_for_entry(self, field, value: str) -> bool:
        """ Проверяем наличие записи в базе данных """

        is_entry_availability = session.query(
            exists().where(field==value)
        ).scalar()
        return is_entry_availability


    def get_model_instance(
            self,
            model: Union[Topic, Task],
            fields: Dict[str, str],
            type: Optional[str] = 'orm') -> Union[Topic, Task, Dict]:
        """ Получаем экземпляр модели """

        try:
            instance = session.query(model).filter_by(**fields).first()
            if type == 'orm': return instance
            elif type == 'dict': return instance.__dict__
        except AttributeError:
            message = 'Экземпляр модели не найден'
            raise ValueError(message)


    def get_or_create_instance(
            self,
            model: Union[Topic, Task],
            fields: Dict[str, str]) -> Union[Topic, Task]:
        """ Получаем или создаем указанную запись """

        instance = session.query(model).filter_by(**fields).first()
        if instance: return instance

        instance = model(**fields)
        session.add(instance)
        session.commit()
        return instance


    def get_full_information_on_task(self, fields: Dict[str, str]) -> Dict[str, Any]:
        """ Получаем полную информацию о задаче """

        task = session.query(Task).filter_by(
            **fields
        ).options(
            joinedload(Task.topics)
        ).first().__dict__
        task['topics'] = ', '.join([topic.name for topic in task['topics']])
        return task


    def bring_list_of_entries_in_orm_objects(
            self,
            model: Union[Topic, Task],
            list_of_entries: List[str],
            search_field: str) -> List[Union[Topic, Task]]:
        """ Приводим список записей в объекты orm """

        result = []

        for entry_element in list_of_entries:
            object_orm = self.get_or_create_instance(
                model,
                {search_field: entry_element}
            )
            result.append(object_orm)

        return result


    def add_task_to_db(self, task: TaskObject) -> None:
        """ Добавляем задачу в базу данных """

        fields = {
            'link': task['link'],
            'number': task['number'],
            'name': task['name'],
            'complexity': task['complexity'],
            'number_of_solutions': task['number_of_solutions'],
        }
        if self.check_for_entry(Task.name, task['name']): return
        task_orm = self.get_or_create_instance(Task, fields)

        list_of_topics = DbUtils().bring_list_of_entries_in_orm_objects(
            Topic,
            task['topics'],
            'name'
        )

        for topic in list_of_topics:
            task_orm.topics.append(topic)


    def add_list_of_tasks(self, list_of_tasks: List[TaskObject]) -> None:
        """ Добавляем список задач в базу данных """

        _ = [self.add_task_to_db(task) for task in list_of_tasks]


    def basic_filtering_algorithm_for_tasks(
            self,
            topic_name: str,
            complexity: Union[int, Tuple[int, int]],
            count: Optional[int]) -> List[Task]:
        """ Основной алгоритм фильтрации задач """

        def filter_by_topic(
                list_of_all_tasks: List[Task],
                topic_name: str) -> List[Task]:
            """ Фильтруем по теме """

            list_of_tasks = []

            for task in list_of_all_tasks:
                if len(task.topics) == 1:
                    list_of_tasks.append(task)


            return [task for task in list_of_tasks if task.topics[0].name == topic_name]


        def filter_by_complexity(
                list_of_all_tasks,
                complexity: Union[int, Tuple[int, int]]) -> List[Task]:
            """ Фильтруем задачи по сложности """

            if type(complexity) == int:
                list_of_tasks = list_of_all_tasks.filter(
                    Task.complexity==complexity
                ).all()
            else:
                list_of_tasks = list_of_all_tasks.filter(
                    Task.complexity.between(*complexity)
                ).all()
            return list_of_tasks


        # ВЫШЕ ОПРЕДЕЛЕНИЕ ФУНКЦИЙ

        list_of_all_tasks = session.query(Task).options(
            joinedload(Task.topics)
        )
        list_of_tasks = filter_by_complexity(
            list_of_all_tasks,
            complexity
        )
        list_of_tasks = filter_by_topic(list_of_tasks, topic_name)
        return list_of_tasks


    def get_filtered_list_of_tasks(
            self,
            topic_name: str,
            complexity: Union[int, Tuple[int, int]],
            count: Optional[int] = None,
            filter_algorithm: Optional[TaskFilteringAlgorithm] = TaskFilteringAlgorithm.basic_filtering_algorithm,
            ) -> List[Task]:
        """ Получаем отфильтрованный список задач """

        if filter_algorithm == TaskFilteringAlgorithm.basic_filtering_algorithm:
            return self.basic_filtering_algorithm_for_tasks(
                topic_name=topic_name,
                complexity=complexity,
                count=count
            )
        else:
            message = 'Такой алгоритм отсутствует'
            raise ValueError(message)


    def get_set_of_new_tasks(
            self, user_data: Dict[str, str]) ->  Tuple[str, List[Task]]:
        """ Получаем набор новых задач """

        def get_data_of_desired_type(data: Dict[str, str]) -> Dict[str, str]:
            """ Получаем данные нужного вида """

            if '-' in user_data.get('task_complexity'):
                user_data['task_complexity'] = tuple(map(int, user_data['task_complexity'].split('-')))
            else:
                user_data['task_complexity'] = int(user_data['task_complexity'])

            return user_data


        def get_tasks_of_desired_type(list_of_tasks: List[Task]) -> List[Dict[str, Any]]:
            """ Получаем задачи нужного вида """

            result = [self.get_full_information_on_task({'name': task.name}) \
                      for task in list_of_tasks]
            return result


        def get_id_of_new_tasks(
                id_of_last_tasks: str,
                list_of_tasks) -> Tuple[str, List[Task]]:
            """ Получаем id новых задач """

            new_set_of_tasks = []

            for task in list_of_tasks:
                if len(new_set_of_tasks) > 10: break
                new_task_id = task.id

                if not id_of_last_tasks:
                    new_set_of_tasks.append(task)
                    id_of_last_tasks += f'{new_task_id} '
                else:
                    if str(new_task_id) in id_of_last_tasks: continue
                    new_set_of_tasks.append(task)
                    id_of_last_tasks += f'{new_task_id} '

            return (id_of_last_tasks, new_set_of_tasks)


        # ВЫШЕ ОПРЕДЕЛЕНИЕ ФУНКЦИЙ

        id_of_last_tasks = user_data.get('id_of_last_tasks')

        data_of_desired_type = get_data_of_desired_type(user_data)

        filtered_list_of_tasks = self.get_filtered_list_of_tasks(
            topic_name=data_of_desired_type['task_topic'],
            complexity=data_of_desired_type['task_complexity'],
        )

        id_of_new_tasks, new_set_of_tasks = get_id_of_new_tasks(
            id_of_last_tasks,
            filtered_list_of_tasks,
        )

        new_set_of_tasks = get_tasks_of_desired_type(new_set_of_tasks)

        id_of_last_tasks += id_of_new_tasks

        return (id_of_last_tasks, new_set_of_tasks)
